using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using Mud.Common.Messages;

namespace Mud.Common.Communication
{

    /// <summary>
    /// Wrapper around TCP/IP sockets which provides non blocking package transportation.
    /// </summary>
    public class PackageTransporter
    {

        #region Properties

        /// <summary>
        /// Underlying socket.
        /// </summary>
        private Socket _socket;

        /// <summary>
        /// Buffer used to construct incoming messages.
        /// </summary>
        private byte[] _incomingBuffer;
        private int _incomingBufferPosition;

        /// <summary>
        /// Current outgoing message that will be sent.
        /// </summary>
        private byte[] _outgoingBuffer;
        private int _outgoingBufferPosition;

        /// <summary>
        /// Default size of the incoming and outgoing buffers.
        /// </summary>
        private const int DefaultBufferSize = 40000;

        /// <summary>
        /// PackageTransporter requires the same client from the oposite side, handshake is required because of that.
        /// </summary>
        public static string Handshake
        {
            get { return _handshake; }
        }
        private const string _handshake = "MessageTransporter1.0";


        /// <summary>
        /// Current connection status.
        /// </summary>
        /// <value></value>
        public TransporterStatus Status
        {
            get { return _status; }
        }
        private TransporterStatus _status = TransporterStatus.Disconnected;


        /// <summary>
        /// The queue of packages received.
        /// </summary>
        public Queue<BytePackage> IncomingPackageQueue
        {
            get { return _incomingPackageQueue; }
        }
        private Queue<BytePackage> _incomingPackageQueue = new Queue<BytePackage>();

        /// <summary>
        /// The queue of messages which are to be transported.
        /// </summary>
        public Queue<BytePackage> OutgoingPackageQueue
        {
            get { return _outgoingPackageQueue; }
        }
        private Queue<BytePackage> _outgoingPackageQueue = new Queue<BytePackage>();

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="T:PackageTransporter"/> class.
        /// </summary>
        public PackageTransporter()
        {
            this._socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
            SetSocketOptions();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:PackageTransporter"/> class.
        /// This constructer is used to construct the PackageTransporter when accepting
        /// new incoming connections
        /// </summary>
        /// <param name="socket">Already initialized socket.</param>
        private PackageTransporter(Socket socket)
        {
            this._socket = socket;
        }

        #region Connection methods

        /// <summary>
        /// Connect this PackageTransporter to remote host.
        /// </summary>
        /// <param name="ipAddress">Remote host address.</param>
        /// <param name="ipPort">Remote host port.</param>
        /// <returns>True if successful, false if there was an error.</returns>
        public bool Connect(string ipAddress, int ipPort)
        {
            if (string.IsNullOrEmpty(ipAddress))
                throw new ArgumentNullException("ipAddress");
            if (ipPort <= 0 || ipPort > 65535)
                throw new ArgumentOutOfRangeException("ipPort");

            IPHostEntry entry;
            try
            {
                entry = Dns.GetHostEntry(ipAddress);
                return Connect(entry.AddressList[0], ipPort);
            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Connect", "Unable to connect to {0}:{1}. Error: {2}.", ipAddress, ipPort, ex);
                return false;
            }
        }

        /// <summary>
        /// Connect this PackageTransporter to remote host.
        /// </summary>
        /// <param name="ipAddress">Remote host address.</param>
        /// <param name="ipPort">Remote host port.</param>
        /// <returns>True if successful, false if there was an error.</returns>
        public bool Connect(IPAddress ipAddress, int ipPort)
        {
            if (ipAddress == null)
                throw new ArgumentNullException("ipAddress");
            if (ipPort <= 0 || ipPort > 65535)
                throw new ArgumentOutOfRangeException("ipPort");

            if (_status != TransporterStatus.Disconnected)
                throw new InvalidOperationException("PackageTransporter be in status 'Disconnected'.");

            try
            {
                this._socket.Blocking = true;
                this._socket.Connect(ipAddress, ipPort);
                this._status = TransporterStatus.Connected;
                Logger.Log(LogType.Comm, "PackageTransporter.Connect", "Connected to {0}:{1}.", ipAddress, ipPort);
                this._socket.Blocking = false;
                if (!CheckPostConnect())
                {
                    this.SetError("PackageTransporter.Connect", "Unable to connect to {0}:{1}. Unable to do a handshake.", ipAddress, ipPort);
                    return false;
                }
                Logger.Log(LogType.Comm, "PackageTransporter.Connect", "Connection to {0}:{1} confirmed.", ipAddress, ipPort);
                return true;
            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Connect", "Unable to connect to {0}:{1}. Error: {2}.", ipAddress, ipPort, ex);
                return false;
            }
        }

        /// <summary>
        /// Listen for incoming connection on specific address and port.
        /// </summary>
        /// <param name="ipAddress">Local host address.</param>
        /// <param name="ipPort">Local host port.</param>
        /// <returns>True if successful, false if there was an error.</returns>
        public bool Listen(string localIpAddress, int ipPort)
        {
            if (string.IsNullOrEmpty(localIpAddress))
                throw new ArgumentNullException("localIpAddress");
            if (ipPort <= 0 || ipPort > 65535)
                throw new ArgumentOutOfRangeException("ipPort");

            IPHostEntry entry;
            try
            {
                entry = Dns.GetHostEntry(localIpAddress);
                return Listen(entry.AddressList[0], ipPort);
            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Listen", "Unable to resolve address {0}. Error {1}.", localIpAddress, ex);
                return false;
            }
        }

        /// <summary>
        /// Listen for incoming connection on specific address and port.
        /// </summary>
        /// <param name="ipAddress">Local host address.</param>
        /// <param name="ipPort">Local host port.</param>
        /// <returns>True if successful, false if there was an error.</returns>
        public bool Listen(IPAddress localIpAddress, int ipPort)
        {
            if (localIpAddress == null)
                throw new ArgumentNullException("ipAddress");
            if (ipPort <= 0 || ipPort > 65535)
                throw new ArgumentOutOfRangeException("ipPort");

            if (_status != TransporterStatus.Disconnected)
                throw new InvalidOperationException("PackageTransporter must be in status 'Disconnected'.");

            try
            {
                this._socket.ExclusiveAddressUse = true;
                this._socket.Bind(new IPEndPoint(localIpAddress, ipPort));
                this._socket.Listen(20);
                Logger.Log(LogType.Comm, "PackageTransporter.Listen", "Listening on {0}:{1} for incoming connections.", localIpAddress, ipPort);
                this._status = TransporterStatus.Listening;
                return true;

            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Listen", "Unable to set-up listening on port {0}. Error: {1}.", ipPort, ex);
                return false;
            }
        }

        /// <summary>
        /// If there is a connection attempt on a listening port then this method will accept the connection.
        /// This method will return immediately whether there is a new connection or not.
        /// </summary>
        /// <returns>New connection or null if there was an error or the timeout has expired.</returns>
        public PackageTransporter Accept()
        {
            return Accept(0);
        }


        /// <summary>
        /// If there is a connection attempt on a listening port then this method will accept the connection.
        /// </summary>
        /// <param name="timeOut">How long to wait for incoming connection in miliseconds.</param>
        /// <returns>New connection or null if there was an error or the timeout has expired.</returns>
        public PackageTransporter Accept(int timeOut)
        {
            if (_status != TransporterStatus.Listening)
                throw new InvalidOperationException("PackageTransporter must be in status 'Listening'.");

            try
            {
                if (!this._socket.Poll(timeOut * 1000, SelectMode.SelectRead))
                    return null;

                PackageTransporter newTransporter = new PackageTransporter(this._socket.Accept());
                newTransporter._status = TransporterStatus.Connected;
                if (!newTransporter.CheckPostAccept())
                {
                    Logger.Log(LogType.Error, "PackageTransporter.Accept", "Unable to accept connection from to {0}. Unable to do a handshake.", newTransporter._socket.RemoteEndPoint);
                    newTransporter.Close();
                    newTransporter.Dispose();
                    return null;
                }
                Logger.Log(LogType.Comm, "PackageTransporter.Accept", "New connection from {0}.", newTransporter._socket.RemoteEndPoint);
                return newTransporter;

            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Accept", "Unable to accept incoming connections on {0}. Error: {1}.", this._socket.LocalEndPoint, ex);
                return null;
            }
        }

        /// <summary>
        /// Polls the collection of MessageTransporters for incoming data.
        /// </summary>
        /// <param name="transporters">Collections of transporters to poll.</param>
        /// <param name="timeout">Timeout in milliseconds</param>
        /// <returns>List containing the matching transporters.</returns>
        public static IList<PackageTransporter> Poll(IList<PackageTransporter> transporters, int timeout)
        {
            List<Socket> sockets = new List<Socket>();
            List<PackageTransporter> readable = new List<PackageTransporter>();
            foreach (PackageTransporter transporter in transporters)
            {
                sockets.Add(transporter._socket);
            }
            Socket.Select(sockets, null, null, timeout * 1000);
            foreach (Socket socket in sockets)
            {
                foreach (PackageTransporter transporter in transporters)
                {
                    if (transporter._socket == socket)
                    {
                        readable.Add(transporter);
                        break;
                    }
                }
            }
            return readable;
        }

        /// <summary>
        /// Closes the connection.
        /// </summary>
        public void Close()
        {
            try
            {
                _socket.Close(0);
                this._status = TransporterStatus.Disconnected;
            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Close", "Unable to close socket. Error: {0}.", ex);
            }
        }



        #endregion

        #region Communication methods

        /// <summary>
        /// Attempts to read data from the underlying socket and to construct packages from it.
        /// </summary>
        /// <returns>True if at least one package is in the queue. 
        /// False if no packages are in the queue.</returns>
        public bool Read()
        {
            if (_status != TransporterStatus.Connected)
                return false;

            try
            {
                int dataInSocket = _socket.Available;
                if (dataInSocket == 0)
                    return false;

                if (_incomingBuffer == null)
                    _incomingBuffer = new byte[DefaultBufferSize];

                int remainingBufferSize = _incomingBuffer.Length - _incomingBufferPosition;
                if (remainingBufferSize == 0)
                {
                    this.SetError("PackageTransporter.Read", "Incoming message is greater then the size of the buffer.");
                    return false;
                }

                int dataToRead = remainingBufferSize < dataInSocket ? remainingBufferSize : dataInSocket;
                int bytesRead = _socket.Receive(_incomingBuffer, _incomingBufferPosition, dataToRead, SocketFlags.None);
                _incomingBufferPosition += bytesRead;
                ConstructPackagesFromBuffer();

                // if we have more data to read then read again
                if (dataInSocket > dataToRead)
                    return Read();


                return _incomingPackageQueue.Count > 0;

            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Read", "Unable to read from socket. Error: {0}.", ex);
                return false;
            }

        }


        /// <summary>
        /// Attempts to write the messages in the outgoing message queue to the underlying socket.
        /// </summary>
        /// <returns>True if at least one message was sent. 
        /// False if no messages were sent.</returns>
        public bool Write()
        {
            if (_status != TransporterStatus.Connected)
                return false;

            if (_outgoingBuffer == null && _outgoingPackageQueue.Count == 0)
                return false;

            try
            {
                if (!this._socket.Poll(0, SelectMode.SelectWrite))
                    return false;

                if (_outgoingBuffer != null) // there is data left from the previous attempt
                {
                    int dataToWrite = _outgoingBuffer.Length - _outgoingBufferPosition;
                    int dataWritten = _socket.Send(_outgoingBuffer, _outgoingBufferPosition, dataToWrite, SocketFlags.None);
                    if (dataWritten == 0)
                    {
                        this.SetError("PackageTransporter.Write", "Unable to write to socket.");
                        return false;
                    }
                    if (dataWritten == dataToWrite) // we have written everything
                    {
                        _outgoingBufferPosition = 0;
                        _outgoingBuffer = null;
                    }
                    else // we still have data to write
                    {
                        _outgoingBufferPosition += dataWritten;
                    }

                    return true;
                }
                else if (_outgoingPackageQueue.Count > 0) // there is no data to be sent but there are messages to send
                {
                    _outgoingBuffer = _outgoingPackageQueue.Dequeue().Unpack();
                    return Write();
                }
                else
                {
                    return false;
                }

            }
            catch (Exception ex)
            {
                this.SetError("PackageTransporter.Write", "Unable to write to socket. Error: {0}.", ex);
                return false;
            }

        }


        /// <summary>
        /// Checks if there is data to be read in the underlying socket.
        /// </summary>
        public bool DataAvailable()
        {
            if (_status != TransporterStatus.Connected)
                throw new InvalidOperationException("PackageTransporter must be in status 'Connected'.");

            try
            {
                return _socket.Available > 0;
            }
            catch (SocketException ex)
            {
                this.SetError("PackageTransporter.DataAvailable", "There was a problem while checking data on the connection. Error: {0}.", ex);
                return true;
            }
        }

        /// <summary>
        /// Checks if the listening connection can accept a connection.
        /// </summary>
        public bool AcceptAvailable()
        {
            if (_status != TransporterStatus.Connected && _status != TransporterStatus.Listening)
                throw new InvalidOperationException("PackageTransporter must be in status 'Listening'.");

            try
            {
                return _socket.Poll(0, SelectMode.SelectRead);
            }
            catch (SocketException ex)
            {
                this.SetError("PackageTransporter.AcceptAvailable", "There was a problem while checking if a new connection can be accepted connection. Error: {0}.", ex);
                return true;
            }
        }



        #endregion

        #region Helper methods

        private bool CheckPostConnect()
        {
            byte[] handshakeBytes = Encoding.ASCII.GetBytes(Handshake);
            byte[] responseBytes = new byte[handshakeBytes.Length];

            if(_socket.Send(handshakeBytes) != handshakeBytes.Length)
                return false;

            // wait 5 seconds for response
            if (!_socket.Poll(5000000, SelectMode.SelectRead))
                return false;

            if (_socket.Receive(responseBytes, responseBytes.Length, SocketFlags.None) != handshakeBytes.Length)
                return false;

            string answer = Encoding.ASCII.GetString(responseBytes);

            if (answer != Handshake)
                return false;

            return true;
        }

        private bool CheckPostAccept()
        {
            byte[] handshakeBytes = Encoding.ASCII.GetBytes(Handshake);
            byte[] responseBytes = new byte[handshakeBytes.Length];

            // wait 5 seconds for response
            if (!_socket.Poll(5000000, SelectMode.SelectRead))
                return false;

            if (_socket.Receive(responseBytes, responseBytes.Length, SocketFlags.None) != handshakeBytes.Length)
                return false;

            string answer = Encoding.ASCII.GetString(responseBytes);
            if (answer != Handshake)
                return false;
          
            if (_socket.Send(handshakeBytes) != handshakeBytes.Length)
                return false;

            return true;
        }

        private void SetSocketOptions()
        {
            _socket.Blocking = false;
            _socket.ReceiveBufferSize = 24000;
            _socket.SendBufferSize = 24000;
            _socket.NoDelay = true;
            _socket.LingerState = new LingerOption(true, 5);
        }

        private void SetError(string source, string message, params object[] arguments)
        {
            Logger.Log(LogType.Error, source, message, arguments);
            this._status = TransporterStatus.Error;
        }

        /// <summary>
        /// Attempts to construct packages from the buffer.
        /// </summary>
        private void ConstructPackagesFromBuffer()
        {
            int packageLength;
            if (!BytePackage.CanPack(_incomingBuffer, 0, _incomingBufferPosition))
                return;

            BytePackage package = BytePackage.Pack(_incomingBuffer, 0, _incomingBufferPosition, out packageLength);

            _incomingPackageQueue.Enqueue(package);
            if (packageLength < _incomingBufferPosition)
            {
                Buffer.BlockCopy(_incomingBuffer, packageLength, _incomingBuffer, 0, _incomingBufferPosition - packageLength);
                _incomingBufferPosition -= packageLength;
                ConstructPackagesFromBuffer(); // try again
            }
            else
            {
                _incomingBufferPosition = 0;
            }

        }


        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            try
            {
                this.Close();
                ((IDisposable)_socket).Dispose();

            }
            catch { }
        }

        #endregion


    }


    /// <summary>
    /// Possible PackageTransporter statuses.
    /// </summary>
    public enum TransporterStatus
    {
        /// <summary>
        /// Transporter has an error and can not be used.
        /// </summary>
        Error = -1,
        /// <summary>
        /// Transporter is disconnected, can be used for connecting or for accepting.
        /// </summary>
        Disconnected = 0,
        /// <summary>
        /// Transporter is connected to remote host.
        /// </summary>
        Connected = 1,
        /// <summary>
        /// Transporter is set up for listening for remote connections.
        /// </summary>
        Listening = 2
    }

}
